package jinyilw.tools.net.file;

import jinyilw.tools.net.NetTools;
import jinyilw.tools.net.dev.PipedInputStream;
import jinyilw.tools.net.dev.PipedOutputStream;
import jinyilw.tools.net.dev.TCPClient;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class FileTransfer
{
	private static final int BUFFER_SIZE = 1024 * 20;
	private static final short sendFilesFlag = 1;
	private static final short getFilesFlag = 2;
	private static final short sendNullFlag = 3;
	private static final short setIDFlag = 8;

	private final Object[] locks = new Object[15];
	private volatile Short currentLockIndex = -1;

	private short clientID = -1;

	private File receiveRootFolder;
	private File sendRootFolder;

	private boolean fileReceiving;
	private File[] receivingFiles;
	private File[] receivedFiles;
	private long[] fileLens;
	private byte[] storageBuffer;
	private FileOutputStream fileOutput;
	private Short fileIndex;
	private long remainLen;
	private Short lockIndex;

	protected SocketChannel socketChannel;
	protected Selector selector;

	private ByteBuffer readBuffer;
	private ByteBuffer writeBuffer;

	private ByteArrayOutputStream byteOutput;
	private DataOutputStream dataOutput;

	private DataInputStream dataInput;
	private PipedOutputStream storage;
	private boolean waitingForLength = true;
	private boolean waitingForInfo = true;
	private short length;
	private short flag;

	protected SocketAddress hostAddress;
	private FileTransferServer transferServer;

	public FileTransfer()
	{
		init();
	}

	public FileTransfer(String host, int port)
	{
		this(new InetSocketAddress(host, port));
	}

	public FileTransfer(SocketAddress host)
	{
		hostAddress = host;
		init();
	}

	protected FileTransfer(FileTransferServer server,
			SocketChannel clientChannel, Selector readSelector)
			throws IOException
	{
		transferServer = server;
		receiveRootFolder = server.receiveRootFolder;
		sendRootFolder = server.sendRootFolder;
		socketChannel = clientChannel;
		selector = readSelector;
		socketChannel.configureBlocking(false);
		socketChannel.socket().setTcpNoDelay(true);
		socketChannel.register(selector, SelectionKey.OP_READ, this);
		init();
	}

	private void init()
	{
		byteOutput = new ByteArrayOutputStream(1024);
		dataOutput = new DataOutputStream(byteOutput);
		storage = new PipedOutputStream();
		try
		{
			dataInput = new DataInputStream(
					new PipedInputStream(storage, BUFFER_SIZE * 5));
		} catch (IOException e)
		{
			e.printStackTrace();
		}

		storageBuffer = new byte[BUFFER_SIZE];
		readBuffer = ByteBuffer.allocate(BUFFER_SIZE);
		writeBuffer = ByteBuffer.allocate(BUFFER_SIZE);
		readBuffer.order(ByteOrder.BIG_ENDIAN);
		writeBuffer.order(ByteOrder.BIG_ENDIAN);

		if (transferServer == null)
			sendRootFolder = receiveRootFolder = new File(
					System.getProperty("user.dir"));
	}

	public void setHostAddress(String host, int port)
	{
		hostAddress = new InetSocketAddress(host, port);
	}

	public void setHostAddress(SocketAddress hostAddress)
	{
		this.hostAddress = hostAddress;
	}

	synchronized void createConnect()
	{
		if (selector == null)
			try
			{
				selector = Selector.open();
			} catch (IOException e)
			{
				selector = null;
				socketChannel = null;
				return;
			}
		if (socketChannel != null)
			try
			{
				socketChannel.close();
			} catch (IOException e)
			{
				e.printStackTrace();
			}
		try
		{
			socketChannel = SocketChannel.open();
			socketChannel.connect(hostAddress);
			socketChannel.configureBlocking(false);
			socketChannel.socket().setTcpNoDelay(true);
			socketChannel.register(selector, SelectionKey.OP_READ);
		} catch (IOException e)
		{
			try
			{
				socketChannel.close();
			} catch (IOException ignored)
			{
			}
			socketChannel = null;
		}
	}

	void update(long elapsedTime) throws IOException
	{
		if (socketChannel == null)
			throw new IOException("连接异常！");
		selector.select();
		Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
		while (iterator.hasNext())
		{
			SelectionKey key = iterator.next();
			iterator.remove();
			read();
		}
	}

	protected synchronized void read() throws IOException
	{
		if (socketChannel == null)
			throw new IOException("连接异常！");

		readBuffer.clear();
		int bytesRead = socketChannel.read(readBuffer);
		if (bytesRead < 0)
		{
			throw new IOException("Reached end of stream");
		} else if (bytesRead == 0)
			return;

		storage.write(readBuffer.array(), 0, bytesRead);

		while (dataInput.available() > 0)
		{
			if (waitingForLength)
			{
				if (dataInput.available() > 2)
				{
					length = dataInput.readShort();
					waitingForLength = false;
				} else
					break;
			} else if (waitingForInfo)
			{
				if (dataInput.available() >= length)
				{
					flag = dataInput.readShort();
					if (flag == sendFilesFlag)
					{
						readSendFilesInfo();
						waitingForInfo = false;
					} else if (flag == getFilesFlag)
					{
						dealGetFiles();
					} else if (flag == setIDFlag)
					{
						dealSetID();
					} else if (flag == sendNullFlag)
					{
						dealSendNull();
					}
				}
			} else
			{
				if (flag == sendFilesFlag)
					dealSendFiles();
			}
		}
	}

	private void readSendFilesInfo() throws IOException
	{
		if (NetTools.debug)
			System.out.println("> 开始接收文件...");
		lockIndex = dataInput.readShort();
		fileReceiving = true;
		short num = dataInput.readShort();
		receivingFiles = new File[num];
		fileLens = new long[num];
		for (int i = 0; i < num; i++)
		{
			receivingFiles[i] = new File(receiveRootFolder,
					dataInput.readUTF());
			fileLens[i] = dataInput.readLong();
		}
		fileIndex = 0;
		fileOutput = new FileOutputStream(receivingFiles[fileIndex]);
		remainLen = fileLens[fileIndex];
	}

	private void dealSendFiles() throws IOException
	{
		int n;
		int available;
		while ((available = dataInput.available()) > 0 || remainLen == 0)
		{
			if (remainLen <= available)
			{
				if (remainLen > 0)
				{
					byte[] reBuffer = new byte[(int) remainLen];
					dataInput.readFully(reBuffer);
					fileOutput.write(reBuffer);
					remainLen = 0;
				}
			} else
			{
				n = dataInput.read(storageBuffer);
				fileOutput.write(storageBuffer, 0, n);
				remainLen -= n;
			}
			if (remainLen == 0)
			{
				fileOutput.close();
				System.out.println(receivingFiles[fileIndex].getName() + " 完成");
				if (fileIndex == receivingFiles.length - 1)
				{
					waitingForLength = true;
					waitingForInfo = true;
					fileReceiving = false;
					receivedFiles = receivingFiles;
					Object lock = getLock(lockIndex);
					synchronized (lock)
					{
						lock.notifyAll();
					}
					if (NetTools.debug)
						System.out.println("> 接收文件成功...\n");
					return;
				}
				fileIndex++;
				fileOutput = new FileOutputStream(receivingFiles[fileIndex]);
				remainLen = fileLens[fileIndex];
			}
		}
	}

	private void dealGetFiles() throws IOException
	{
		short lockIndex = dataInput.readShort();
		short num = dataInput.readShort();
		File[] sendingFiles = new File[num];
		for (int i = 0; i < num; i++)
			sendingFiles[i] = new File(sendRootFolder, dataInput.readUTF());
		new Thread(() ->
		{
			try
			{
				sendFiles(lockIndex, sendingFiles);
			} catch (IOException e)
			{
				e.printStackTrace();
			}
		}).start();
		waitingForLength = true;
		waitingForInfo = true;
	}

	private void dealSetID() throws IOException
	{
		clientID = dataInput.readShort();
		waitingForLength = true;
		waitingForInfo = true;
		if (transferServer != null)
		{
			TCPClient client = transferServer.findTCPClient(clientID);
			if (client != null)
			{
				client.setFileTransfer(this);
				if (NetTools.debug)
					System.out.println("设置文件传输客户端ID号:" + clientID);
			}
		}
	}

	private void dealSendNull()
	{
		receivedFiles = null;
		synchronized (this)
		{
			notifyAll();
		}
		waitingForLength = true;
		waitingForInfo = true;
	}

	private void sendExistFiles(Short lockIndex, File[] existFiles)
			throws IOException
	{
		if (NetTools.debug)
			System.out.println("> 开始发送文件...");
		byteOutput.reset();
		dataOutput.writeShort(sendFilesFlag);
		dataOutput.writeShort(lockIndex);
		dataOutput.writeShort(existFiles.length);
		for (File file : existFiles)
		{
			dataOutput.writeUTF(file.getName());
			dataOutput.writeLong(file.length());
			System.out.println(file.getName() + "; 长度:" + file.length());
		}
		dataOutput.flush();
		byte[] data = byteOutput.toByteArray();
		writeBuffer.clear();
		writeBuffer.putShort((short) data.length);
		writeBuffer.put(data);
		writeBuffer.rewind();
		writeBuffer.limit(data.length + 2);
		socketChannel.write(writeBuffer);
		writeBuffer.limit(2048);
		writeBuffer.clear();
		for (File file : existFiles)
		{
			FileInputStream fileInput = new FileInputStream(file);
			FileChannel fileChannel = fileInput.getChannel();
			while (fileChannel.position() < fileChannel.size())
			{
				fileChannel.read(writeBuffer);
				writeBuffer.flip();
				while (writeBuffer.hasRemaining())
					socketChannel.write(writeBuffer);
				writeBuffer.clear();
			}
			fileInput.close();
		}
		if (NetTools.debug)
			System.out.println("> 发送文件完成...\n");
	}

	private void sendNullFiles() throws IOException
	{
		writeBuffer.clear();
		writeBuffer.putShort((short) 2);
		writeBuffer.putShort(sendNullFlag);
		writeBuffer.rewind();
		writeBuffer.limit(4);
		socketChannel.write(writeBuffer);
		writeBuffer.limit(2048);
	}

	private synchronized void sendGetFileNames(Short lockIndex,
			String... filePaths) throws IOException
	{
		if (NetTools.debug)
			System.out.println("> 开始获取文件...");
		byteOutput.reset();
		dataOutput.writeShort(getFilesFlag);
		dataOutput.writeShort(lockIndex);
		dataOutput.writeShort(filePaths.length);
		for (String filePath : filePaths)
		{
			dataOutput.writeUTF(filePath);
		}
		dataOutput.flush();
		byte[] data = byteOutput.toByteArray();
		writeBuffer.clear();
		writeBuffer.putShort((short) data.length);
		writeBuffer.put(data);
		writeBuffer.rewind();
		writeBuffer.limit(data.length + 2);
		socketChannel.write(writeBuffer);
		writeBuffer.limit(2048);
	}

	public synchronized void sendFiles(File... files) throws IOException
	{
		sendFiles((short) -1, files);
	}

	synchronized void sendFiles(Short lockIndex, File... files)
			throws IOException
	{
		if (socketChannel == null || files == null)
			return;
		File[] existFiles = getExistFiles(files);
		if (existFiles != null)
			sendExistFiles(lockIndex, existFiles);
		else
			sendNullFiles();
	}

	public void getFiles(String... filePaths)
	{
		if (socketChannel == null || filePaths == null || filePaths.length == 0)
			return;
		try
		{
			sendGetFileNames((short) -1, filePaths);
		} catch (IOException e)
		{
			e.printStackTrace();
		}
	}

	public File[] getFilesAndWait(int timeout, String... fileNames)
	{
		if (socketChannel == null || fileNames == null || fileNames.length == 0)
			return null;
		try
		{
			short lockIndex = getLockIndex();
			sendGetFileNames(lockIndex, fileNames);
			Object lock = getLock(lockIndex);
			if (timeout <= 0)
				timeout = 1000000;
			synchronized (lock)
			{
				lock.wait(timeout);
			}
		} catch (IOException e)
		{
			e.printStackTrace();
			return null;
		} catch (InterruptedException ignored)
		{
		}
		return receivedFiles;
	}

	synchronized short getLockIndex()
	{
		// writeLock.lock();
		currentLockIndex++;
		if (currentLockIndex >= locks.length)
			currentLockIndex = 0;
		if (locks[currentLockIndex] == null)
			locks[currentLockIndex] = new Object();
		short index = currentLockIndex;
		// writeLock.unlock();
		return index;
	}

	private Object getLock(short index)
	{
		Object lock = this;
		if (index >= 0 && index < locks.length)
		{
			lock = locks[index];
			if (lock == null)
				lock = this;
		}
		return lock;
	}

	public File[] getReceivedFiles()
	{
		try
		{
			if (fileReceiving)
			{
				Object lock = getLock(lockIndex);
				synchronized (lock)
				{
					lock.wait();
				}
			} else
			{
				synchronized (this)
				{
					wait(1500);
				}
				if (fileReceiving)
				{
					Object lock = getLock(lockIndex);
					synchronized (lock)
					{
						lock.wait();
					}
				}
			}
		} catch (InterruptedException e)
		{
			e.printStackTrace();
		}
		return receivedFiles;
	}

	public short getClientID()
	{
		return clientID;
	}

	public synchronized void setClientID(short id)
	{
		clientID = id;
		if (socketChannel == null || id == -1)
			return;
		byteOutput.reset();
		try
		{
			dataOutput.writeShort(setIDFlag);
			dataOutput.writeShort(clientID);
			dataOutput.flush();
			byte[] data = byteOutput.toByteArray();
			writeBuffer.clear();
			writeBuffer.putShort((short) data.length);
			writeBuffer.put(data);
			writeBuffer.rewind();
			writeBuffer.limit(data.length + 2);
			socketChannel.write(writeBuffer);
			writeBuffer.limit(2048);
		} catch (IOException e)
		{
			e.printStackTrace();
		}
	}

	public void setReceiveRootFolder(File folder)
	{
		receiveRootFolder = folder;
	}

	public void setSendRootFolder(File folder)
	{
		sendRootFolder = folder;
	}

	public synchronized boolean isConnected()
	{
		if (socketChannel == null)
			return false;
		return socketChannel.isConnected();
	}

	public synchronized boolean startConnect()
	{
		System.err.println("未实现客户端连接方法！");
		return false;
	}

	public synchronized void stopConnect()
	{
		SocketChannel channel = socketChannel;
		socketChannel = null;
		if (channel != null)
			try
			{
				channel.close();
			} catch (IOException e)
			{
				e.printStackTrace();
			}
	}

	private static File[] getExistFiles(File... files)
	{
		if (files == null || files.length == 0)
			return null;
		int num = 0;
		boolean[] isFile = new boolean[files.length];
		for (int i = 0; i < files.length; i++)
			if (isFile[i] = files[i] != null && files[i].isFile())
				num++;
		if (num == 0)
			return null;
		File[] existFiles = new File[num];
		for (int i = 0, j = 0; i < isFile.length; i++)
			if (isFile[i])
			{
				existFiles[j] = files[i];
				j++;
			}
		return existFiles;
	}
}
